package nl.zoidberg.calculon.engine;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.TreeMap;

import nl.zoidberg.calculon.model.Piece;

public class MoveGenerator implements Iterator<Move> {
	
	private static Map<Byte, PieceMoveGenerator> generatorMap = new HashMap<Byte, PieceMoveGenerator>();
	private static List<PieceMoveGenerator> generators = new ArrayList<PieceMoveGenerator>();
	
	static {
		generatorMap.put(Piece.PAWN, 	new PawnCaptureGenerator());
		generatorMap.put(Piece.KNIGHT, 	new KnightMoveGenerator());
		generatorMap.put(Piece.BISHOP, 	new BishopMoveGenerator());
		generatorMap.put(Piece.ROOK, 	new RookMoveGenerator());
		generatorMap.put(Piece.QUEEN, 	new QueenMoveGenerator());
		generatorMap.put(Piece.KING, 	new KingMoveGenerator());
		
		generators.add(new PawnCaptureGenerator());
		generators.add(new KnightMoveGenerator());
		generators.add(new BishopMoveGenerator());
		generators.add(new RookMoveGenerator());
		generators.add(new QueenMoveGenerator());
		generators.add(new PawnMoveGenerator());
		generators.add(new KingMoveGenerator());
	}
	
	private Board board;
	private List<Move> queuedMoves = new ArrayList<Move>();
	private int genIndex = 0;
	
	public MoveGenerator(Board board) {
		this.board = board;
	}

	public boolean hasNext() {
		if(queuedMoves.size() == 0) {
			populateMoves();
		}
		return (queuedMoves.size() > 0);
	}

	public Move next() {
		if(queuedMoves.size() == 0) {
			populateMoves();
		}
		if(queuedMoves.size() == 0) {
			throw new NoSuchElementException();
		}
		Move rv = queuedMoves.get(0);
		queuedMoves.remove(0);
		return rv;
	}

	public void remove() {
		throw new UnsupportedOperationException();
	}
	
	private void populateMoves() {
		if(board.isDrawnByRule()) {
			return;
		}
		
		if(genIndex >= generators.size()) {
			return;
		}
		PieceMoveGenerator nextGen = generators.get(genIndex++);
		Map<String, Board> getMoves = nextGen.generateMoves(board);
		for(String key: getMoves.keySet()) {
			queuedMoves.add(new Move(key, getMoves.get(key)));
		}
		if(queuedMoves.size() == 0 && genIndex < generators.size()) {
			populateMoves();
		}
	}
	
	public static boolean isMovePossible(Board board) {
		BitBoard bitBoard = board.getBitBoard();
		long toPlayMap = bitBoard.getBitmapColor(board.getPlayer())&bitBoard.getAllPieces();
		while(toPlayMap != 0) {
			long nextPiece = Long.highestOneBit(toPlayMap);
			toPlayMap ^= nextPiece;
			int[] coord = BitBoard.toCoords(nextPiece);
			
			byte type = (byte) (board.getPiece(coord[0], coord[1]) & Piece.MASK_TYPE);
			Map<String, Board> moves = generatorMap.get(type).generateMoves(board, coord[0], coord[1], true);
			if(moves.size() > 0) {
				return true;
			}
		}
		return false;
	}
	
	public Map<String, Board> generateMoves() {
		Map<String, Board> rv = new TreeMap<String, Board>();
		
		if(board == null || board.isDrawnByRule()) {
			return rv;
		}
		
		BitBoard bitBoard = board.getBitBoard();
		BitBoard scratchPad = bitBoard.clone();
		long myColor = bitBoard.getBitmapColor(board.getPlayer());
		long toPlayMap = myColor & bitBoard.getAllPieces();
		
		int playerIdx = board.getPlayer() == Piece.WHITE ? BitBoard.MAP_WHITE : BitBoard.MAP_BLACK;
		long freePawns = toPlayMap & bitBoard.getBitmapPawns();
		freePawns &= ~(board.getPlayer() == Piece.WHITE ? bitBoard.getAllPieces()>>>8 : bitBoard.getAllPieces()<<8);
		for(long pawns = freePawns; pawns != 0; ) {
			long nextPawn = Long.highestOneBit(pawns);
			pawns ^= nextPawn;
			long bothSquares = nextPawn | (playerIdx == BitBoard.MAP_WHITE ? nextPawn<<8 : nextPawn>>>8);
			scratchPad.bitmaps[BitBoard.MAP_PAWNS] ^= bothSquares;
			scratchPad.bitmaps[playerIdx] ^= bothSquares;
			if( ! CheckDetector.inCheck(scratchPad, board.getPlayer(), ! board.isInCheck())) {
				String move = EngineUtils.toCoord(nextPawn) + EngineUtils.toCoord(bothSquares^nextPawn);
				if((bothSquares & (BitBoard.getRankMap(0)|BitBoard.getRankMap(7))) != 0) {
					rv.put(move + "=Q", board.clone().applyMove(move + "=Q"));
					rv.put(move + "=R", board.clone().applyMove(move + "=R"));
					rv.put(move + "=N", board.clone().applyMove(move + "=N"));
					rv.put(move + "=B", board.clone().applyMove(move + "=B"));
				} else {
					rv.put(move, board.clone().applyMove(move));
				}
			}
			// Unmake move
			scratchPad.bitmaps[BitBoard.MAP_PAWNS] ^= bothSquares;
			scratchPad.bitmaps[playerIdx] ^= bothSquares;
		}
		
		freePawns &= ~(board.getPlayer() == Piece.WHITE ? bitBoard.getAllPieces()>>>16 : bitBoard.getAllPieces()<<16);
		freePawns &= (BitBoard.getRankMap(board.getPlayer() == Piece.WHITE ? 1 : 6));
		for(long pawns = freePawns; pawns != 0; ) {
			long nextPawn = Long.highestOneBit(pawns);
			pawns ^= nextPawn;
			long bothSquares = nextPawn | (playerIdx == BitBoard.MAP_WHITE ? nextPawn<<16 : nextPawn>>>16);
			scratchPad.bitmaps[BitBoard.MAP_PAWNS] ^= bothSquares;
			scratchPad.bitmaps[playerIdx] ^= bothSquares;
			if( ! CheckDetector.inCheck(scratchPad, board.getPlayer(), ! board.isInCheck())) {
				String move = EngineUtils.toCoord(nextPawn) + EngineUtils.toCoord(bothSquares^nextPawn);
				rv.put(move, board.clone().applyMove(move));
			}
			// Unmake move
			scratchPad.bitmaps[BitBoard.MAP_PAWNS] ^= bothSquares;
			scratchPad.bitmaps[playerIdx] ^= bothSquares;
		}
		
		while(toPlayMap != 0) {
			long nextPiece = Long.highestOneBit(toPlayMap);
			toPlayMap ^= nextPiece;
			int[] coord = BitBoard.toCoords(nextPiece);
			byte type = (byte) (board.getPiece(coord[0], coord[1]) & Piece.MASK_TYPE);
			rv.putAll(generatorMap.get(type).generateMoves(board, coord[0], coord[1]));
		}
		
		return rv;
	}

	public static Map<String, List<String>> getPossibleMoves(Board board) {
		Map<String, List<String>> moves = new HashMap<String, List<String>>();
		for(String move: new MoveGenerator(board).generateMoves().keySet()) {
			if("O-O".equals(move)) {
				move = board.getPlayer() == Piece.WHITE ? "E1G1" : "E8G8";
			}
			if("O-O-O".equals(move)) {
				move = board.getPlayer() == Piece.WHITE ? "E1C1" : "E8C8";
			}
			String from = move.substring(0, 2);
			if(moves.get(from) == null) {
				moves.put(from, new ArrayList<String>());
			}
			List<String> toList = moves.get(from);
			toList.add(move.substring(2));
		}
		return moves;
	}
}
